﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using Toenda.Foundation.Serialization;
using System.IO;
using Toenda.Foundation.Utility;

namespace Toenda.Foundation.Security.Cryptography {
	/// <summary>
	/// SymmCrypto is a wrapper of System.Security.Cryptography.SymmetricAlgorithm classes
	/// and simplifies the interface. It supports customized SymmetricAlgorithm as well.
	/// </summary>
	public class SymmetricEncryption {
		public class EncryptData {
			public byte[] Data { get; set; }
			public int Length { get; set; }
		}

		/// <remarks>
		/// Supported .Net intrinsic SymmetricAlgorithm classes.
		/// </remarks>
		public enum SymmProvEnum : int {
			DES, RC2, Rijndael
		}

		private SymmetricAlgorithm mobjCryptoService;

		/// <remarks>
		/// Constructor for using an intrinsic .Net SymmetricAlgorithm class.
		/// </remarks>
		public SymmetricEncryption(SymmProvEnum netSelected) {
			switch(netSelected) {
				case SymmProvEnum.DES:
					mobjCryptoService = new DESCryptoServiceProvider();
					break;

				case SymmProvEnum.RC2:
					mobjCryptoService = new RC2CryptoServiceProvider();
					break;

				case SymmProvEnum.Rijndael:
					mobjCryptoService = new RijndaelManaged();
					break;
			}
		}

		/// <remarks>
		/// Constructor for using a customized SymmetricAlgorithm class.
		/// </remarks>
		public SymmetricEncryption(SymmetricAlgorithm ServiceProvider) {
			mobjCryptoService = ServiceProvider;
		}

		public string GenerateKey(int length) {
			if(length < 0) {
				length = 1;
			}

			if(length > 32) {
				length = 32;
			}

			return Helper.GenerateRandomString(length, true);
		}

		public string GenerateIV() {
			return Helper.GenerateRandomString(16, false);
		}

		public EncryptData Encrypt(byte[] source, byte[] salt, byte[] vector) {
			byte[] bytOut;
			int i = 0;

			// create a MemoryStream so that the process can be done without I/O files
			MemoryStream memoryStream = new MemoryStream();
		
			// set the private key
			mobjCryptoService.Key = salt;

			if(vector.Length > 0) {
				mobjCryptoService.IV = vector;
			}

			// create an Encryptor from the Provider Service instance
			ICryptoTransform encrypto = mobjCryptoService.CreateEncryptor();

			// create Crypto Stream that transforms a stream using the encryption
			CryptoStream cryptoStream = new CryptoStream(memoryStream, encrypto, CryptoStreamMode.Write);

			// write out encrypted content into MemoryStream
			cryptoStream.Write(source, 0, source.Length);
			cryptoStream.FlushFinalBlock();

			// get the output and trim the '\0' bytes
			bytOut = memoryStream.GetBuffer();

			for(i = 0; i < bytOut.Length; i++) {
				if(bytOut[i] == 0) {
					break;
				}
			}

			cryptoStream.Close();
			cryptoStream.Dispose();

			encrypto.Dispose();

			memoryStream.Close();
			memoryStream.Dispose();

			return new EncryptData() { Data = bytOut, Length = i };
		}

		public string EncryptString(string source, string key, string vector) {
			byte[] byteIn = ASCIIEncoding.ASCII.GetBytes(source);
			byte[] byteKey = this.GetLegalKey(key);
			byte[] byteVector = (!vector.IsNullOrTrimmedEmpty() ? this.GetLegalKey(vector) : new byte[0]);

			EncryptData data = this.Encrypt(byteIn, byteKey, byteVector);

			return Convert.ToBase64String(data.Data, 0, data.Length);
		}

		public string EncryptString(string source, string key) {
			return EncryptString(source, key, null);
		}

		public byte[] Decrypt(byte[] source, byte[] salt, byte[] vector) {
			List<byte> buffer = new List<byte>();

			MemoryStream memoryStream = new MemoryStream(source, 0, source.Length);

			// set the private key
			mobjCryptoService.Key = salt;

			if(vector.Length > 0) {
				mobjCryptoService.IV = vector;
			}

			// create a Decryptor from the Provider Service instance
			try {
				ICryptoTransform encrypto = mobjCryptoService.CreateDecryptor();

				// create Crypto Stream that transforms a stream using the decryption
				CryptoStream cryptoStream = new CryptoStream(memoryStream, encrypto, CryptoStreamMode.Read);

				int single = 0;

				while((single = cryptoStream.ReadByte()) > -1) {
					buffer.Add((byte)single);
				}

				cryptoStream.Close();
				cryptoStream.Dispose();

				encrypto.Dispose();
			}
			catch(Exception ex) {
				buffer.Clear();

				foreach(byte b in source) {
					buffer.Add(b);
				}
			}

			memoryStream.Close();
			memoryStream.Dispose();

			return buffer.ToArray();
		}

		public string DecryptString(string source, string key, string vector) {
			byte[] byteIn = Convert.FromBase64String(source);
			byte[] byteKey = GetLegalKey(key);
			byte[] byteVector = (!vector.IsNullOrTrimmedEmpty() ? this.GetLegalKey(vector) : new byte[0]);

			byte[] byteOut = this.Decrypt(byteIn, byteKey, byteVector);
			string plainText = ASCIIEncoding.ASCII.GetString(byteOut);

			return plainText;
		}

		public string DecryptString(string source, string key) {
			return DecryptString(source, key, null);
		}

		public string EncryptStringDouble(string source, string key, DateTime timeKey) {
			string encrypt = this.EncryptString(source, timeKey.ToString());

			Dictionary<DateTime, string> toCryptDict = new Dictionary<DateTime, string>();
			toCryptDict.Add(timeKey, encrypt);

			string toCrypt = Serialize.ToJsonString<Dictionary<DateTime, string>>(toCryptDict);
			toCrypt = Convert.ToBase64String(toCrypt.ToByteArray());

			return this.EncryptString(toCrypt, key);
		}

		public string DecryptStringDouble(string source, string key) {
			string plainText = "";

			try {
				string toDecrypt = this.DecryptString(source, key);

				toDecrypt = Convert.FromBase64String(toDecrypt).ToAsciiString();

				Dictionary<DateTime, string> decryptDict = Serialize.FromJsonString<Dictionary<DateTime, string>>(
					toDecrypt
				);

				if(decryptDict != null
				&& decryptDict.Count > 0) {
					KeyValuePair<DateTime, string> pair = decryptDict.First();
					return this.DecryptString(pair.Value, pair.Key.ToString());
				}
				else {
					plainText = source;
				}
			}
			catch(Exception) {
				plainText = source;
			}

			return plainText;
		}

		/// <remarks>
		/// Depending on the legal key size limitations of a specific CryptoService provider
		/// and length of the private key provided, padding the secret key with space character
		/// to meet the legal size of the algorithm.
		/// </remarks>
		private byte[] GetLegalKey(string key) {
			string sTemp;

			if(mobjCryptoService.LegalKeySizes.Length > 0) {
				int lessSize = 0, moreSize = mobjCryptoService.LegalKeySizes[0].MinSize;

				// key sizes are in bits
				while(key.Length * 8 > moreSize) {
					lessSize = moreSize;
					moreSize += mobjCryptoService.LegalKeySizes[0].SkipSize;
				}

				sTemp = key.PadRight(moreSize / 8, ' ');
			}
			else {
				sTemp = key;
			}

			// convert the secret key to byte array
			return ASCIIEncoding.ASCII.GetBytes(sTemp);
		}
	}
}
